﻿// -----------------------------------------------------------------------
// <copyright file="Index.cs" company="JSC Centido">
//  Copyright (c) JSC Centido. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------

using System.Data.Odbc;

namespace Centido.Core
{
    using System;
    using System.Collections.Generic;


    /// <summary>
    ///   A table index defined on a user table.
    /// </summary>
    /// <remarks>Table indexes are used to optimize the exucution
    /// of some common queries to the table. By default a user-defined table
    /// has two default indexes. The first one guarantees the uniqueness of the
    /// primary key column. The second one helps to read active rows.</remarks>
    /// <example>
    ///   The following example shows how to add indexes for a user-defined table.
    /// <code>
    ///     using System;
    ///     using System.Data.Common;
    ///     using Centido.Core;
    /// 
    ///     class DomainBuildSample
    ///     {
    ///         static void Main()
    ///         {
    ///             var domainConfig = new DomainConfiguration()
    ///             {
    ///                 DbConnectionString = @"Driver={SQL Server};Server=.\SQLEXPRESS;Database=MyDb;Uid=admin;Pwd=P@ssw0rd;",
    ///                 DbProviderName = "MSSQLSERVER"
    ///             };
    ///         
    ///             var domain = Domain.Build(domainConfig);
    ///         
    ///             domain.DeleteAll();
    /// 
    ///             //Creating a table that will store a student list
    ///             var studentTable = domain.AddTable("students");
    /// 
    ///             //Creating a column that will store a student surname
    ///             var surnameColumn = studentTable.AddColumn("surname", ColumnType.String)
    ///                 as StringColumn;
    ///             surnameColumn.IsNotEmpty = true;
    /// 
    ///             //Creating an index that will guarantee the uniqueness of the student surname
    ///             studentTable.AddIndex(true, surnameColumn);
    /// 
    ///             //Inserting a student
    ///             studentTable.GrantAll();
    /// 
    ///             studentTable.InsertRow(surnameColumn.Value("Smith"));
    /// 
    ///             //Trying to add a student with the same surname
    ///             try
    ///             {
    ///                 studentTable.InsertRow(surnameColumn.Value("Smith"));
    ///             }
    ///             catch(DbException)
    ///             {
    ///                 Console.WriteLine("Cannot add a student with the duplicate surname");
    ///             }
    ///         }
    ///     }
    /// </code>
    /// </example>
    public sealed class Index : IIdentifiedById, IRemovable, IRefreshable
    {
        /// <summary>
        ///   The maximum number of column in an index.
        /// </summary>
        /// <remarks>
        ///   The actual value of the maximum number of column in an index constant
        ///   is set because of Sql Server limitations.
        /// </remarks>
        public const int IndexedColumnMaxCount = 16;


        /// <summary>
        ///   The parent table
        /// </summary>
        private readonly Table _table;

        /// <summary>
        ///   The uniqueness of the index.
        /// </summary>
        private bool? _isUnique;

        /// <summary>
        ///   Initializes a new instance of the <see cref = "Centido.Core.Index" /> class
        ///   for the specified index id and parent table.
        /// </summary>
        /// <param name = "indexId">The specified index id.</param>
        /// <param name = "table">The specified parent table.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="table"/> is null.</exception>
        internal Index(Guid indexId, Table table)
        {
            if (table == null)
                throw new ArgumentNullException("table");

            Id = indexId;
            _table = table;
        }

        /// <summary>
        ///   Gets the parent table.
        /// </summary>
        public Table Table
        {
            get { return _table; }
        }

        /// <summary>
        ///   Gets the parent domain.
        /// </summary>
        public Domain Domain
        {
            get { return _table.Domain; }
        }

        /// <summary>
        ///   Gets an indexed column list.
        /// </summary>
        public ICollection<Column> Columns
        {
            get
            {
                var resultList = new List<Column>();

                var columnIdList = DbValue.GetBinary(ReadProperty(Db.Tables.IndexList.Col.ColumnIdList));

                if (columnIdList == null)
                    throw new DbOperationException();

                var currentPosition = 0;

                while (currentPosition + IndexedColumnMaxCount - 1 < columnIdList.Length)
                {
                    var columnId = new byte[IndexedColumnMaxCount];

                    for (var i = 0; i < IndexedColumnMaxCount; i++)
                        columnId[i] = columnIdList[currentPosition + i];

                    currentPosition += IndexedColumnMaxCount;

                    resultList.Add(_table.GetColumn(new Guid(columnId)));
                }

                return resultList;
            }
        }

        /// <summary>
        ///   True if the index is unique; otherwise, false.
        /// </summary>
        public bool IsUnique
        {
            get
            {
                if (_isUnique == null)
                    _isUnique = DbValue.GetInteger(ReadProperty(Db.Tables.IndexList.Col.IsUnique)) == 1;

                return _isUnique.Value;
            }
        }

        #region IIdentifiedById Members

        /// <summary>
        ///   Gets the id of the index.
        /// </summary>
        public Guid Id { get; private set; }


        /// <summary>
        ///   Checks the existence of the index.
        /// </summary>
        /// <returns>Returns true if the index still exists; otherwise, returns false.</returns>
        public bool Exists()
        {
            var wasFound = false;

            Domain.DbAccess.ExecuteScalar(
                cmd =>
                {
                    cmd.CommandText = string.Format("select count(*) from {0} where {1}=?",
                        Domain.GetObjectFullName(Db.Tables.IndexList.Name),
                        Db.Tables.IndexList.Col.IndexId);

                    cmd.Parameters.Add(Domain.DbAccess.CreateParameter("id", OdbcType.Binary, GetRawId()));
                },
                result => wasFound = DbValue.GetInteger(result) != 0);

            return wasFound;
        }

        #endregion

        #region IRemovable Members

        /// <summary>
        ///   Delete table index.
        ///   Current user has to have CanChangeSchema=true in order to execute this function
        /// </summary>
        public void Delete()
        {
            Domain.DbAccess.ExecuteNonQuery(
                cmd =>
                {
                    cmd.CommandText = string.Format("{{call {0}(?)}}",
                        Domain.GetObjectFullName(Db.Procedures.DeleteIndex));

                    cmd.Parameters.Add(Domain.DbAccess.CreateParameter("a_index_id",
                        OdbcType.Binary, GetRawId()));
                });
        }

        #endregion

        /// <summary>
        ///   Gets the id of the index in the binary format.
        /// </summary>
        /// <returns>Returns the id of the index in the binary format.</returns>
        public byte[] GetRawId()
        {
            return Id.ToByteArray();
        }

        /// <summary>
        ///   Reads the index property.
        /// </summary>
        /// <param name = "propertyName">The property name.</param>
        /// <returns>Returns the property value.</returns>
        private object ReadProperty(string propertyName)
        {
            object propertyValue = null;

            Domain.DbAccess.ExecuteScalar(
                cmd =>
                {
                    cmd.CommandText = string.Format(
                        "select {0} from {1} where {2}=?",
                        propertyName,
                        Domain.GetObjectFullName(Db.Tables.IndexList.Name),
                        Db.Tables.IndexList.Col.IndexId);

                    cmd.Parameters.Add(Domain.DbAccess.CreateParameter("index_id", OdbcType.Binary, GetRawId()));
                },
                result => propertyValue = result);

            return propertyValue;
        }

        /// <summary>
        ///   Refreshes the properties of the index.
        /// </summary>
        public void Refresh()
        {
            _isUnique = null;
        }
    }
}